The given dataset has 1,000 records of users with 10 attributes. The classification variable is whether or not they clicked on an advertisement, denoted 0 or 1 in the last column of the data frame. We are trying to predict which users are more likely to click on the advertisements.
Check the head of the data to see what the data looks like.
#original.data <- read.csv("C:/Users/Bryan/Downloads/train.csv")
head(original.data)
str(original.data)
'data.frame': 1000 obs. of 10 variables:
$ id : int 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 ...
$ Timestamp : Factor w/ 775 levels "2016-01-01 02:52:10",..: 1 2 3 4 5 6 6 7 8 9 ...
$ Daily.Time.Spent.on.Site: num 80.7 68 80.9 78.8 36.6 ...
$ Age : int 34 25 36 28 29 36 42 28 33 59 ...
$ Area.Income : num 58909 68358 60803 63498 42838 ...
$ Daily.Internet.Usage : num 240 188 240 212 196 ...
$ Ad.Topic.Line : Factor w/ 825 levels "Adaptive 24hour Graphic Interface",..: 646 15 662 566 724 388 558 618 107 199 ...
$ gender : int 0 1 0 0 0 0 0 1 1 1 ...
$ Country : Factor w/ 233 levels "Afghanistan",..: 167 1 24 83 217 138 64 132 74 88 ...
$ Clicked : int 0 0 0 0 1 1 1 0 0 1 ...
Let’s check to see if there are any N/A values in the data set.
any(is.na(original.data))
[1] TRUE
library(Amelia)
missmap(original.data)

According to the missmap from the Amelia library, Area Income appears to be the only one missing some data. Let’s start visualizing the data first to see if Area Income has a major impact on whether or not someone clicked on the ads. Suppose it does; we will write a funciton that will impute age as accurately as possible.
library(ggplot2)
pl <- ggplot(original.data, aes(Area.Income, Daily.Time.Spent.on.Site)) + geom_point(aes(color = factor(Clicked)))
pl

Notice that income does not appear to play a major role in whether or not people clicked on the ads. People of all incomes, according to this plot, click on the ads. The more important factor here appears to be the daily time spent on the site. Let’s examine another variable similar to this one that resides within the data frame.
pl2 <- ggplot(original.data, aes(Daily.Internet.Usage, Daily.Time.Spent.on.Site)) + geom_point(aes(color = factor(Clicked)))
pl2

There is still a significant amount of clustering here, but in my opinion, it appears to be a tighter cluster in terms of the Area Income. Let’s examine Daily Internet Usage mapped by Area Income.
pl3 <- ggplot(original.data, aes(Area.Income, Daily.Internet.Usage)) + geom_point(aes(color = factor(Clicked)))
pl3

This plot looks pretty similar to the first one we examined. Given these two variables alone, we have a pretty good idea of whether or not a user clicked on the ads. Let’s do more EDA then begin working on training our model.
library(plotly)
pl4 <- ggplot(original.data, aes(Age)) + geom_histogram(fill = "blue", color = "black", alpha = 0.5, bins = 40)
pl4

pl5 <- ggplot(original.data, aes(Daily.Internet.Usage)) + geom_histogram(fill = "blue", color = "black", alpha = 0.5, bins = 40)
pl5

pl6 <- ggplot(original.data, aes(gender)) + geom_bar(aes(fill = factor(gender), alpha = 0.5))
pl6

pl7 <- ggplot(original.data, aes(Clicked, Area.Income)) + geom_boxplot(aes(group=Clicked, fill=factor(Clicked), alpha = 0.4))
ggplotly(pl7)
Removed 225 rows containing non-finite values (stat_boxplot).
We see from the plots above some very interesting information. The people that clicked on the ads spend less time on the internet in general and on the site each day. They also tend to have lower incomes, per the boxplot. Let’s write a function to impute Area.Income into the missing spots.
Using plotly, we can easily get the median Area Income to impute for each group: 62,430.55 where Clicked=0; 50,306.31 where Clicked=1.
impute_income <- function(income, clicked) {
out <- income
for (i in 1:length(income)){
if(is.na(income[i]))
{
if(clicked[i] == 0)
{
out[i] <- 62430.55
}
else
{
out[i] <- 50306.31
}
}
else
{
out[i] <- income[i]
}
}
return(out)
}
original.incomes <- original.data$Area.Income
fixed.incomes <- impute_income(original.data$Area.Income, original.data$Clicked)
original.data$Area.Income <- fixed.incomes
Let’s check to see that it worked properly!
missmap(original.data, col = c("yellow", "black"))

Great! Having no missing data is a good feeling. Now, let’s start running some models. Begin with K-means and the relevant columns.
library(cluster)
df.relevant <- data.frame(original.data$Daily.Time.Spent.on.Site, original.data$Daily.Internet.Usage)
cluster.click <- kmeans(df.relevant, 2, nstart = 10)
Let’s look at the clusplot!
library(cluster)
clusplot(df.relevant, cluster.click$cluster, color = TRUE, shade = TRUE, labels = 0, lines = 0)

library(factoextra)
sil <- silhouette(cluster.click$cluster, dist(df.relevant))
fviz_silhouette(sil)

Above 0.5; looks pretty good!
Let’s start the KNN model:
var(train2[, 1])
[1] 83416.67
var(train2[, 3])
[1] 249.0543
clicked <- train2[, 10]
knn.df <- data.frame(train2$Daily.Time.Spent.on.Site, train2$Daily.Internet.Usage)
#CBIND the classification column
knn.df <- cbind(knn.df, clicked)
knn.standardized <- scale(knn.df[, -3])
var(knn.standardized[, 1])
[1] 1
var(knn.standardized[, 2])
[1] 1
#Test - first 300 rows for test set
test.index <- 1:300
test.data <- knn.standardized[test.index, ]
test.clicked <- clicked[test.index]
#Train
train.data <- knn.standardized[-test.index, ]
train.clicked <- clicked[-test.index]
#Run the model
library(class)
predictions.clicked <- knn(train.data, test.data, train.clicked, k=3)
mean(test.clicked != predictions.clicked)
[1] 0.1633333
Let’s observe the model with other k-values
predictions.clicked <- NULL
error.rate <- NULL
for (i in 1:25) {
predictions.clicked <- knn(train.data, test.data, train.clicked, k=i)
error.rate[i] <- mean(test.clicked != predictions.clicked)
}
k.values <- 1:25
error.df <- data.frame(error.rate, k.values)
pl8 <- ggplot(error.df, aes(k.values, error.rate)) + geom_point() + geom_line(lty="dotted", color = "red")
ggplotly(pl8)
So, the model runs most accurately with k=3; so that is what we will choose.
final.predictions.clicked <- knn(train.data, test.data, train.clicked, k=3)
mean(test.clicked != final.predictions.clicked)
[1] 0.1633333
The model is approximately 83.67% accurate in it’s predicitons.
LS0tDQp0aXRsZTogIlIgTm90ZWJvb2sgLSBBZHZlcnRpc2luZyBEYXRhIC0gQnJ5YW4gSG9uZWNrIg0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KVGhlIGdpdmVuIGRhdGFzZXQgaGFzIDEsMDAwIHJlY29yZHMgb2YgdXNlcnMgd2l0aCAxMCBhdHRyaWJ1dGVzLiBUaGUgY2xhc3NpZmljYXRpb24gdmFyaWFibGUgaXMgd2hldGhlciBvciBub3QgdGhleSBjbGlja2VkIG9uIGFuIGFkdmVydGlzZW1lbnQsIGRlbm90ZWQgMCBvciAxIGluIHRoZSBsYXN0IGNvbHVtbiBvZiB0aGUgZGF0YSBmcmFtZS4gV2UgYXJlIHRyeWluZyB0byBwcmVkaWN0IHdoaWNoIHVzZXJzIGFyZSBtb3JlIGxpa2VseSB0byBjbGljayBvbiB0aGUgYWR2ZXJ0aXNlbWVudHMuDQoNCkNoZWNrIHRoZSBoZWFkIG9mIHRoZSBkYXRhIHRvIHNlZSB3aGF0IHRoZSBkYXRhIGxvb2tzIGxpa2UuDQpgYGB7cn0NCiNvcmlnaW5hbC5kYXRhIDwtIHJlYWQuY3N2KCJDOi9Vc2Vycy9Ccnlhbi9Eb3dubG9hZHMvdHJhaW4uY3N2IikNCg0KaGVhZChvcmlnaW5hbC5kYXRhKQ0KYGBgDQpgYGB7cn0NCnN0cihvcmlnaW5hbC5kYXRhKQ0KYGBgDQoNCkxldCdzIGNoZWNrIHRvIHNlZSBpZiB0aGVyZSBhcmUgYW55IE4vQSB2YWx1ZXMgaW4gdGhlIGRhdGEgc2V0Lg0KYGBge3J9DQphbnkoaXMubmEob3JpZ2luYWwuZGF0YSkpDQpsaWJyYXJ5KEFtZWxpYSkNCg0KbWlzc21hcChvcmlnaW5hbC5kYXRhKQ0KYGBgDQpBY2NvcmRpbmcgdG8gdGhlIG1pc3NtYXAgZnJvbSB0aGUgQW1lbGlhIGxpYnJhcnksIEFyZWEgSW5jb21lIGFwcGVhcnMgdG8gYmUgdGhlIG9ubHkgb25lIG1pc3Npbmcgc29tZSBkYXRhLiBMZXQncyBzdGFydCB2aXN1YWxpemluZyB0aGUgZGF0YSBmaXJzdCB0byBzZWUgaWYgQXJlYSBJbmNvbWUgaGFzIGEgbWFqb3IgaW1wYWN0IG9uIHdoZXRoZXIgb3Igbm90IHNvbWVvbmUgY2xpY2tlZCBvbiB0aGUgYWRzLiBTdXBwb3NlIGl0IGRvZXM7IHdlIHdpbGwgd3JpdGUgYSBmdW5jaXRvbiB0aGF0IHdpbGwgaW1wdXRlIGFnZSBhcyBhY2N1cmF0ZWx5IGFzIHBvc3NpYmxlLg0KYGBge3J9DQpsaWJyYXJ5KGdncGxvdDIpDQpwbCA8LSBnZ3Bsb3Qob3JpZ2luYWwuZGF0YSwgYWVzKEFyZWEuSW5jb21lLCBEYWlseS5UaW1lLlNwZW50Lm9uLlNpdGUpKSArIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gZmFjdG9yKENsaWNrZWQpKSkNCnBsDQpgYGANCk5vdGljZSB0aGF0IGluY29tZSBkb2VzIG5vdCBhcHBlYXIgdG8gcGxheSBhIG1ham9yIHJvbGUgaW4gd2hldGhlciBvciBub3QgcGVvcGxlIGNsaWNrZWQgb24gdGhlIGFkcy4gUGVvcGxlIG9mIGFsbCBpbmNvbWVzLCBhY2NvcmRpbmcgdG8gdGhpcyBwbG90LCBjbGljayBvbiB0aGUgYWRzLiBUaGUgbW9yZSBpbXBvcnRhbnQgZmFjdG9yIGhlcmUgYXBwZWFycyB0byBiZSB0aGUgZGFpbHkgdGltZSBzcGVudCBvbiB0aGUgc2l0ZS4gTGV0J3MgZXhhbWluZSBhbm90aGVyIHZhcmlhYmxlIHNpbWlsYXIgdG8gdGhpcyBvbmUgdGhhdCByZXNpZGVzIHdpdGhpbiB0aGUgZGF0YSBmcmFtZS4gDQpgYGB7cn0NCnBsMiA8LSBnZ3Bsb3Qob3JpZ2luYWwuZGF0YSwgYWVzKERhaWx5LkludGVybmV0LlVzYWdlLCBEYWlseS5UaW1lLlNwZW50Lm9uLlNpdGUpKSArIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gZmFjdG9yKENsaWNrZWQpKSkNCnBsMg0KYGBgDQpUaGVyZSBpcyBzdGlsbCBhIHNpZ25pZmljYW50IGFtb3VudCBvZiBjbHVzdGVyaW5nIGhlcmUsIGJ1dCBpbiBteSBvcGluaW9uLCBpdCBhcHBlYXJzIHRvIGJlIGEgdGlnaHRlciBjbHVzdGVyIGluIHRlcm1zIG9mIHRoZSBBcmVhIEluY29tZS4gTGV0J3MgZXhhbWluZSBEYWlseSBJbnRlcm5ldCBVc2FnZSBtYXBwZWQgYnkgQXJlYSBJbmNvbWUuDQpgYGB7cn0NCnBsMyA8LSBnZ3Bsb3Qob3JpZ2luYWwuZGF0YSwgYWVzKEFyZWEuSW5jb21lLCBEYWlseS5JbnRlcm5ldC5Vc2FnZSkpICsgZ2VvbV9wb2ludChhZXMoY29sb3IgPSBmYWN0b3IoQ2xpY2tlZCkpKQ0KcGwzDQpgYGANClRoaXMgcGxvdCBsb29rcyBwcmV0dHkgc2ltaWxhciB0byB0aGUgZmlyc3Qgb25lIHdlIGV4YW1pbmVkLiBHaXZlbiB0aGVzZSB0d28gdmFyaWFibGVzIGFsb25lLCB3ZSBoYXZlIGEgcHJldHR5IGdvb2QgaWRlYSBvZiB3aGV0aGVyIG9yIG5vdCBhIHVzZXIgY2xpY2tlZCBvbiB0aGUgYWRzLiBMZXQncyBkbyBtb3JlIEVEQSB0aGVuIGJlZ2luIHdvcmtpbmcgb24gdHJhaW5pbmcgb3VyIG1vZGVsLg0KYGBge3J9DQpsaWJyYXJ5KHBsb3RseSkNCg0KcGw0IDwtIGdncGxvdChvcmlnaW5hbC5kYXRhLCBhZXMoQWdlKSkgKyBnZW9tX2hpc3RvZ3JhbShmaWxsID0gImJsdWUiLCBjb2xvciA9ICJibGFjayIsIGFscGhhID0gMC41LCBiaW5zID0gNDApDQpwbDQNCnBsNSA8LSBnZ3Bsb3Qob3JpZ2luYWwuZGF0YSwgYWVzKERhaWx5LkludGVybmV0LlVzYWdlKSkgKyBnZW9tX2hpc3RvZ3JhbShmaWxsID0gImJsdWUiLCBjb2xvciA9ICJibGFjayIsIGFscGhhID0gMC41LCBiaW5zID0gNDApDQpwbDUNCnBsNiA8LSBnZ3Bsb3Qob3JpZ2luYWwuZGF0YSwgYWVzKGdlbmRlcikpICsgZ2VvbV9iYXIoYWVzKGZpbGwgPSBmYWN0b3IoZ2VuZGVyKSwgYWxwaGEgPSAwLjUpKQ0KcGw2DQpwbDcgPC0gZ2dwbG90KG9yaWdpbmFsLmRhdGEsIGFlcyhDbGlja2VkLCBBcmVhLkluY29tZSkpICsgZ2VvbV9ib3hwbG90KGFlcyhncm91cD1DbGlja2VkLCBmaWxsPWZhY3RvcihDbGlja2VkKSwgYWxwaGEgPSAwLjQpKQ0KZ2dwbG90bHkocGw3KQ0KYGBgDQpXZSBzZWUgZnJvbSB0aGUgcGxvdHMgYWJvdmUgc29tZSB2ZXJ5IGludGVyZXN0aW5nIGluZm9ybWF0aW9uLiBUaGUgcGVvcGxlIHRoYXQgY2xpY2tlZCBvbiB0aGUgYWRzIHNwZW5kIGxlc3MgdGltZSBvbiB0aGUgaW50ZXJuZXQgaW4gZ2VuZXJhbCBhbmQgb24gdGhlIHNpdGUgZWFjaCBkYXkuIFRoZXkgYWxzbyB0ZW5kIHRvIGhhdmUgbG93ZXIgaW5jb21lcywgcGVyIHRoZSBib3hwbG90LiBMZXQncyB3cml0ZSBhIGZ1bmN0aW9uIHRvIGltcHV0ZSBBcmVhLkluY29tZSBpbnRvIHRoZSBtaXNzaW5nIHNwb3RzLg0KDQpVc2luZyBwbG90bHksIHdlIGNhbiBlYXNpbHkgZ2V0IHRoZSBtZWRpYW4gQXJlYSBJbmNvbWUgdG8gaW1wdXRlIGZvciBlYWNoIGdyb3VwOiA2Miw0MzAuNTUgd2hlcmUgQ2xpY2tlZD0wOyA1MCwzMDYuMzEgd2hlcmUgQ2xpY2tlZD0xLg0KYGBge3J9DQogaW1wdXRlX2luY29tZSA8LSBmdW5jdGlvbihpbmNvbWUsIGNsaWNrZWQpIHsNCiAgIG91dCA8LSBpbmNvbWUNCiAgIGZvciAoaSBpbiAxOmxlbmd0aChpbmNvbWUpKXsNCiAgICAgaWYoaXMubmEoaW5jb21lW2ldKSkNCiAgICB7DQogICAgICAgaWYoY2xpY2tlZFtpXSA9PSAwKQ0KICAgICAgICAgew0KICAgICAgICAgICAgb3V0W2ldIDwtIDYyNDMwLjU1DQogICAgICAgICB9DQogICAgICAgZWxzZQ0KICAgICAgICAgew0KICAgICAgICAgICAgb3V0W2ldIDwtIDUwMzA2LjMxDQogICAgICAgICB9DQogICAgfQ0KICAgICBlbHNlDQogICAgew0KICAgICAgIG91dFtpXSA8LSBpbmNvbWVbaV0NCiAgICB9DQogICB9DQogICByZXR1cm4ob3V0KQ0KfQ0KDQpvcmlnaW5hbC5pbmNvbWVzIDwtIG9yaWdpbmFsLmRhdGEkQXJlYS5JbmNvbWUNCg0KZml4ZWQuaW5jb21lcyA8LSBpbXB1dGVfaW5jb21lKG9yaWdpbmFsLmRhdGEkQXJlYS5JbmNvbWUsIG9yaWdpbmFsLmRhdGEkQ2xpY2tlZCkNCg0Kb3JpZ2luYWwuZGF0YSRBcmVhLkluY29tZSA8LSBmaXhlZC5pbmNvbWVzDQpgYGANCkxldCdzIGNoZWNrIHRvIHNlZSB0aGF0IGl0IHdvcmtlZCBwcm9wZXJseSENCmBgYHtyfQ0KbWlzc21hcChvcmlnaW5hbC5kYXRhLCBjb2wgPSBjKCJ5ZWxsb3ciLCAiYmxhY2siKSkNCmBgYA0KR3JlYXQhIEhhdmluZyBubyBtaXNzaW5nIGRhdGEgaXMgYSBnb29kIGZlZWxpbmcuIE5vdywgbGV0J3Mgc3RhcnQgcnVubmluZyBzb21lIG1vZGVscy4gQmVnaW4gd2l0aCBLLW1lYW5zIGFuZCB0aGUgcmVsZXZhbnQgY29sdW1ucy4NCmBgYHtyfQ0KbGlicmFyeShjbHVzdGVyKQ0KDQpkZi5yZWxldmFudCA8LSBkYXRhLmZyYW1lKG9yaWdpbmFsLmRhdGEkRGFpbHkuVGltZS5TcGVudC5vbi5TaXRlLCBvcmlnaW5hbC5kYXRhJERhaWx5LkludGVybmV0LlVzYWdlKQ0KDQpjbHVzdGVyLmNsaWNrIDwtIGttZWFucyhkZi5yZWxldmFudCwgMiwgbnN0YXJ0ID0gMTApDQpgYGANCkxldCdzIGxvb2sgYXQgdGhlIGNsdXNwbG90IQ0KYGBge3J9DQpsaWJyYXJ5KGNsdXN0ZXIpDQoNCmNsdXNwbG90KGRmLnJlbGV2YW50LCBjbHVzdGVyLmNsaWNrJGNsdXN0ZXIsIGNvbG9yID0gVFJVRSwgc2hhZGUgPSBUUlVFLCBsYWJlbHMgPSAwLCBsaW5lcyA9IDApDQpgYGANCmBgYHtyfQ0KbGlicmFyeShmYWN0b2V4dHJhKQ0KDQpzaWwgPC0gc2lsaG91ZXR0ZShjbHVzdGVyLmNsaWNrJGNsdXN0ZXIsIGRpc3QoZGYucmVsZXZhbnQpKQ0KZnZpel9zaWxob3VldHRlKHNpbCkNCmBgYA0KQWJvdmUgMC41OyBsb29rcyBwcmV0dHkgZ29vZCENCg0KTGV0J3Mgc3RhcnQgdGhlIEtOTiBtb2RlbDoNCmBgYHtyfQ0KdmFyKHRyYWluMlssIDFdKQ0KdmFyKHRyYWluMlssIDNdKQ0KDQpjbGlja2VkIDwtIHRyYWluMlssIDEwXQ0KDQprbm4uZGYgPC0gZGF0YS5mcmFtZSh0cmFpbjIkRGFpbHkuVGltZS5TcGVudC5vbi5TaXRlLCB0cmFpbjIkRGFpbHkuSW50ZXJuZXQuVXNhZ2UpDQoNCiNDQklORCB0aGUgY2xhc3NpZmljYXRpb24gY29sdW1uDQprbm4uZGYgPC0gY2JpbmQoa25uLmRmLCBjbGlja2VkKQ0KDQprbm4uc3RhbmRhcmRpemVkIDwtIHNjYWxlKGtubi5kZlssIC0zXSkNCg0KDQp2YXIoa25uLnN0YW5kYXJkaXplZFssIDFdKQ0KdmFyKGtubi5zdGFuZGFyZGl6ZWRbLCAyXSkNCmBgYA0KDQpgYGB7cn0NCiNUZXN0IC0gZmlyc3QgMzAwIHJvd3MgZm9yIHRlc3Qgc2V0DQp0ZXN0LmluZGV4IDwtIDE6MzAwDQp0ZXN0LmRhdGEgPC0ga25uLnN0YW5kYXJkaXplZFt0ZXN0LmluZGV4LCBdDQp0ZXN0LmNsaWNrZWQgPC0gY2xpY2tlZFt0ZXN0LmluZGV4XQ0KDQojVHJhaW4NCnRyYWluLmRhdGEgPC0ga25uLnN0YW5kYXJkaXplZFstdGVzdC5pbmRleCwgXQ0KdHJhaW4uY2xpY2tlZCA8LSBjbGlja2VkWy10ZXN0LmluZGV4XSANCmBgYA0KDQoNCmBgYHtyfQ0KI1J1biB0aGUgbW9kZWwNCmxpYnJhcnkoY2xhc3MpDQoNCnByZWRpY3Rpb25zLmNsaWNrZWQgPC0ga25uKHRyYWluLmRhdGEsIHRlc3QuZGF0YSwgdHJhaW4uY2xpY2tlZCwgaz0zKQ0KDQptZWFuKHRlc3QuY2xpY2tlZCAhPSBwcmVkaWN0aW9ucy5jbGlja2VkKQ0KYGBgDQpMZXQncyBvYnNlcnZlIHRoZSBtb2RlbCB3aXRoIG90aGVyIGstdmFsdWVzDQpgYGB7cn0NCnByZWRpY3Rpb25zLmNsaWNrZWQgPC0gTlVMTA0KZXJyb3IucmF0ZSA8LSBOVUxMDQoNCmZvciAoaSBpbiAxOjI1KSB7DQogIHByZWRpY3Rpb25zLmNsaWNrZWQgPC0ga25uKHRyYWluLmRhdGEsIHRlc3QuZGF0YSwgdHJhaW4uY2xpY2tlZCwgaz1pKQ0KICBlcnJvci5yYXRlW2ldIDwtIG1lYW4odGVzdC5jbGlja2VkICE9IHByZWRpY3Rpb25zLmNsaWNrZWQpDQp9DQpgYGANCg0KYGBge3J9DQprLnZhbHVlcyA8LSAxOjI1DQoNCmVycm9yLmRmIDwtIGRhdGEuZnJhbWUoZXJyb3IucmF0ZSwgay52YWx1ZXMpDQoNCnBsOCA8LSBnZ3Bsb3QoZXJyb3IuZGYsIGFlcyhrLnZhbHVlcywgZXJyb3IucmF0ZSkpICsgZ2VvbV9wb2ludCgpICsgZ2VvbV9saW5lKGx0eT0iZG90dGVkIiwgY29sb3IgPSAicmVkIikNCmdncGxvdGx5KHBsOCkNCmBgYA0KU28sIHRoZSBtb2RlbCBydW5zIG1vc3QgYWNjdXJhdGVseSB3aXRoIGs9Mzsgc28gdGhhdCBpcyB3aGF0IHdlIHdpbGwgY2hvb3NlLg0KYGBge3J9DQpmaW5hbC5wcmVkaWN0aW9ucy5jbGlja2VkIDwtIGtubih0cmFpbi5kYXRhLCB0ZXN0LmRhdGEsIHRyYWluLmNsaWNrZWQsIGs9MykNCg0KbWVhbih0ZXN0LmNsaWNrZWQgIT0gZmluYWwucHJlZGljdGlvbnMuY2xpY2tlZCkNCmBgYA0KVGhlIG1vZGVsIGlzIGFwcHJveGltYXRlbHkgODMuNjclIGFjY3VyYXRlIGluIGl0J3MgcHJlZGljaXRvbnMuDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQo=